package org.dru.clay.respository.ivy;

import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.dru.clay.logger.Logger;
import org.dru.clay.logger.LoggerFactory;
import org.dru.clay.respository.Configuration;
import org.dru.clay.respository.artifact.Artifact;
import org.dru.clay.respository.artifact.Group;
import org.dru.clay.respository.artifact.Module;
import org.dru.clay.respository.artifact.UnresolvedArtifact;
import org.dru.clay.respository.artifact.VersionPattern;
import org.dru.clay.respository.dependency.ConfigurationMapping;
import org.dru.clay.respository.dependency.Dependency;
import org.dru.clay.respository.exception.ResolveException;
import org.dru.clay.util.xml.Xml;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

public class IvyXmlParser {
	private final Logger logger = LoggerFactory.getLogger(IvyXmlParser.class);

	// private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");

	private final Map<String, Configuration> configurations = new HashMap<String, Configuration>();
	private final Set<Dependency> dependencies = new HashSet<Dependency>();

	private final Map<String, String> configurationExtensionMap = new HashMap<String, String>();

	private final ConfigurationMappingParser configurationParser = new ConfigurationMappingParser();

	private IvyStatus status;
	private String dependencyDefaultConf;

	public IvyXmlParser() {
	}

	public IvyXml parse(Module module, byte[] content) {
		try {
			final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			factory.setValidating(false);

			int length = content.length;
			while (length > 0 && content[length - 1] != '>') {
				length--;
			}

			final DocumentBuilder builder = factory.newDocumentBuilder();
			final Document document = builder.parse(new ByteArrayInputStream(content, 0, length));
			final Element ivyModule = document.getDocumentElement();

			verifyModuleInformation(module, ivyModule);
			parseConfigurations(module, ivyModule);
			parseArtifacts(module, ivyModule);
			parseDependencies(module, ivyModule);
			resolveExtensions();

			return new IvyXml(module, status, configurations, dependencies);
		} catch (Exception e) {
			throw new ResolveException("Resolve error: " + e.getMessage(), e);
		}
	}

	private void verifyModuleInformation(Module module, Element ivyModule) {
		final Node info = Xml.getSingleChild(ivyModule, "info");

		final String organisation = Xml.getAttribute(info, "organisation");
		final String moduleName = Xml.getAttribute(info, "module");
		final String revision = Xml.getAttribute(info, "revision");

		this.status = Xml.getEnumAttribute(info, "status", IvyStatus.class, IvyStatus.Integration);
		// final String publication = Xml.getAttribute(info, "publication");

		if (!module.getGroup().get().equals(organisation)) {
			throw new IllegalArgumentException("Ivy description of artifact has different organisation than the resolve artifact: "
					+ organisation);
		}

		final Artifact artifact = module.getArtifact();
		if (!artifact.getName().equals(moduleName)) {
			throw new IllegalArgumentException("Ivy description of artifact has different module than the resolve artifact: " + moduleName);
		}

		if (!artifact.getVersion().plain().equals(revision)) {
			throw new IllegalArgumentException("Ivy description of artifact has different revision than the resolve artifact: " + revision);
		}
	}

	private void parseConfigurations(Module module, Element ivyModule) {
		final Node configs = Xml.getSingleChild(ivyModule, "configurations");
		dependencyDefaultConf = Xml.getAttribute(configs, "defaultConf", "");

		for (Node node : Xml.getChildren(configs, "conf")) {
			final String name = Xml.getAttribute(node, "name");
			if (name == null) {
				throw new IllegalArgumentException("Configuration is missing the required attribute 'name'");
			}

			final String description = Xml.getAttribute(node, "description");
			final IvyVisibility visibility = Xml.getEnumAttribute(node, "visibility", IvyVisibility.class, IvyVisibility.Public);
			final String extending = Xml.getAttribute(node, "extends", null);

			// final boolean transitive = Xml.getBooleanAttribute(conf, "transitive", true);
			// final String deprecated = Xml.getAttribute(conf, "deprecated");

			final Configuration configuration = new Configuration(name, description, visibility);
			configurations.put(name, configuration);
			if (extending != null) {
				configurationExtensionMap.put(name, extending);
			}
		}
	}

	private void parseArtifacts(Module module, Element ivyModule) {
		final Node publications = Xml.getSingleChild(ivyModule, "publications");
		final String defaultConf = Xml.getAttribute(publications, "defaultConf", "*");

		for (Node node : Xml.getChildren(publications, "artifact")) {
			final String name = Xml.getAttribute(node, "name", module.getArtifact().getName());
			final String type = Xml.getAttribute(node, "type", "jar");
			final String ext = Xml.getAttribute(node, "ext", type);
			final String conf = Xml.getAttribute(node, "conf", defaultConf);
			// final String url = Xml.getAttribute(node, "url", type);

			final Artifact artifact = new Artifact(name, module.getArtifact().getVersion(), ext, null);

			final StringTokenizer tokenizer = new StringTokenizer(conf);
			while (tokenizer.hasMoreTokens()) {
				final String token = tokenizer.nextToken().trim();

				if ("*".equals(token)) {
					// insert the artifact in all configurations
					for (Configuration configuration : configurations.values()) {
						configuration.addArtifact(artifact);
					}
					break;
				} else {
					final Configuration configuration = configurations.get(token);
					if (configuration == null) {
						throw new IllegalArgumentException("The artifact " + artifact + " specified unknown configuration " + token);
					}
					configuration.addArtifact(artifact);
				}
			}
		}
	}

	private void parseDependencies(Module module, Element ivyModule) {
		final Node deps = Xml.getSingleChild(ivyModule, "dependencies");

		for (Node node : Xml.getChildren(deps, "dependency")) {
			final Dependency dependency = parseDependency(module, node);
			dependencies.add(dependency);
		}
	}

	private Dependency parseDependency(Module module, Node node) {
		final String group = Xml.getAttribute(node, "org", module.getGroup().get());
		final String name = Xml.getAttribute(node, "name");
		final String rev = Xml.getAttribute(node, "rev");
		// final String revConstraint = Xml.getAttribute(node, "revConstraint", rev);
		final Boolean transitive = Xml.getBooleanAttribute(node, "transitive", true);
		String conf = Xml.getAttribute(node, "conf", dependencyDefaultConf);

		if (name == null) {
			throw new IllegalArgumentException("Dependency is missing attribute name");
		}

		if (rev == null) {
			throw new IllegalArgumentException("Dependency " + name + " is missing attribute rev");
		}

		UnresolvedArtifact artifact = new UnresolvedArtifact(name, new VersionPattern(rev));

		if (!Xml.getChildren(node, "conf").isEmpty()) {
			logger.error("Dependency %s uses nested conf elements which isn't supported : using '*'", artifact);
			conf = "*";
		}
		final ConfigurationMapping mapping = configurationParser.parse(conf);
		return new Dependency(new Group(group), artifact, mapping, transitive);
	}

	private void resolveExtensions() {
		for (Map.Entry<String, String> entry : configurationExtensionMap.entrySet()) {
			final Configuration configuration = configurations.get(entry.getKey());
			if (configuration == null) {
				throw new IllegalArgumentException("Invalid configuration " + entry.getKey() + " used in configuration extension.");
			}

			final StringTokenizer tokenizer = new StringTokenizer(entry.getValue(), ",");
			while (tokenizer.hasMoreTokens()) {
				final String token = tokenizer.nextToken().trim();
				final Configuration source = configurations.get(token);
				if (source == null) {
					throw new IllegalArgumentException("Invalid configuration " + token + " used in configuration extension.");
				}

				for (Artifact artifact : source.getArtifacts()) {
					configuration.addArtifact(artifact);
				}
			}
		}
	}
}
